#!/usr/bin/python2.4
#
# -*- coding: utf-8 -*-
#
###############################################################
# Copyright 2006 Google Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
###############################################################


"""wrapper script to provide functions utilize the OBJson parser.

After convert the OBJson object back to dict, it calls cjson encode method
to further convert it back to JSON raw data feed.

Functions provided:
  IsOBJson:     boolean to return whether a pass-in is a OBJson object.
  SanityCheck:  function to raise error if pass-in is not OBJson object.
  info:         nice output of each callable functions and their docstrings.
  gen:          a generator returns a tuple of each object.
  traverse:     traverse the whole object to return tuple of its contents.
  SearchByTag:  search the object by using tag name
  SearchByVal:  search the object by using the value.
  SearchByNS:   search the object by using namespace.
  GetNSNode:    return all node elements containg namespace declarations.
  NameSpace:    return the NameSpaces in the JSON feed.
  CleanNameSpace: remove all related elements from a given OBJson object
                along with the given namespace declaration.
  ToJson:       convert back to the raw JSON feed.
  _ToJson:      protected method to do the actual work.
  display:      used for interactive mode.
"""

__author__ = 'i3dmaster@gmail.com (Yongjian "Jim" Xu)'


import re
import types
import pprint
import cjson
import warnings
import urllib
import urlparse
import psyco
import objson


__all__ = ['IsOBJson', 'SanityCheck', 'info', 'gen', 'traverse', 'SearchByTag',
    'SearchByVal', 'SearchByNS', 'GetNSNode', 'NameSpaces', 'ToJson',
    'ToDict', 'display',]


# $,    &      +    ,      /    :     ;     =     ?     @     space
_QUOTED_CHAR = \
('%24','%26','%2B','%2C','%2F','%3A','%3B','%3D','%3F','%40','%20',)

def IsOBJson(obj):
  """Function to check if a object is an OBJson initialized object.

  Args:
    obj: target object to check on.

  Returns:
    boolean. Ture when the check object is an OBJson object,
    False otherwise.
  """
  if isinstance(obj, objson.OBJson):
    return True
  else:
    return False

def SanityCheck(obj):
  """Function to perform the sanity check of passing in object.

  Args:
    obj: the target object to check.

  Raises:
    OBJsonTypeError: Source not recognizable.
  """
  if not IsOBJson(obj):
    raise objson.OBJsonTypeError, 'Source type not recognizable'

def info(obj, indent=10):
  """Function to output each callable method with its docstrings.

  Args:
    obj: OBJson object for callable method inspection. Do SanityCheck.
    indent: optional format feeder. Default indent value is 10.
  """
  SanityCheck(obj)
  allcallablelist = [i for i in dir(obj) if callable(getattr(obj, i))]
  for i in allcallablelist:
    if getattr(obj, i).__doc__ is not None:
      print '%s => %s' % (i.ljust(indent),
          ' '.join(getattr(obj, i).__doc__.split()))

def gen(obj):
  """Function to return OBJson (obj,attr,val) key pair one at a time.

  Args:
    obj: An OBJson object.

  Yields:
    A generator object.

  Raises:
    OBJsonRuntimeError: Source not recognizable.
  """
  try:
    if type(obj) is types.ListType:
      return ((i, j, getattr(i, j)) for i in obj for j in \
          i.GetAttrs if not re.search(r'^_{1,2}[^_]+', j))
    else:
      return ((obj, i, getattr(obj, i)) for i in \
        obj.GetAttrs if not re.search(r'^_{1,2}[^_]+', i))
  except:
    raise objson.OBJsonRuntimeError, 'Source not recognizable'

def traverse(obj):
  """Function to traverse the OBJson object tree to
  visit each attribute value pair.

  Args:
    obj: either an OBJson object or a list containing objects.

  Returns:
    A generator returning (obj,attr,val) pair each time called.

  Raises:
    OBJsonRuntimeError,OBJsonTypeError,RuntimeError,TypeError
  """
  try:
    for n in gen(obj):
      if isinstance(n[2], objson.OBJson):
        for j in traverse(n[2]):
          yield j
      elif isinstance(n[2], list):
        for q in n[2]:
          for x in traverse(q):
            yield x
      else:
        yield n
  except (objson.OBJsonRuntimeError, objson.OBJsonTypeError,
      RuntimeError, TypeError), e:
    print 'Error traversing the object: %s' % e

def SearchByTag(obj, key):
  """Function returning a list of tuple with objects
     containing tag/key.

  Args:
    obj: An OBJson object to search on.
    key: a attribute string to search on. (tag/attribute equal
           to the key will be stored)
           Can either be a string, or a re.compiled SRE object.

  Returns:
    A list of tuples containing (obj,attr,value).
  """
  alist = []
  for i in traverse(obj):
    if i[1].find(key) != -1 or i[0].GetName.find(key) != -1:
      alist.append(i)

  if alist:
    if len(alist) == 1:
      return alist[0]
    else:
      return tuple(alist)
  else:
    return None

def SearchByVal(obj, val):
  """Function returning a tuple with objects values containing val substring.

  Args:
    obj: An OBJson object to search on.
    val: text value of a attribute in the obj to search on.
         Can be either a string, regex, or re.compiled object.

  Returns:
    A list of tuples containing (obj,attr,value)
  """
  alist = []
  for i in traverse(obj):
    if i[2].find(val) != -1:
      alist.append(i)

  if alist:
    if len(alist) == 1:
      return alist[0]
    else:
      return tuple(alist)
  else:
    return None

def SearchByNS(obj, ns, sub_string=None):
  """Function returning a tuple containing search result in the given
  namespace.

  Args:
    obj: the OBJson object to search on.
    ns:  the namespace to search on.
    sub_string: (optional) string to filter. If None, return all
                matching elements in the given ns.

  Returns:
    A tuple of search result containing (obj, attribute, value).
  """
  alist = []
  ns_ = ns.lower()

  for i in traverse(obj):
    if i[0].GetName.lower().startswith(ns_):
      if sub_string:
        if i[0].GetName.find(sub_string) != -1 or \
             i[1].find(sub_string) != -1 or i[2].find(sub_string) != -1:
          alist.append(i)
      else:
        alist.append(i)

  if alist:
    if len(alist) == 1:
      return alist[0]
    else:
      return tuple(alist)
  else:
    return None

def _GetNSInfo(obj):
  """Return namespaces info for a given object.

  Args:
    obj: an OBJson object

  Yields:
    a generator return a tuple of (obj, 'ns', 'ns_value')
  """
  SanityCheck(obj)
  for i in traverse(obj):
    if i[1].find('xmlns') != -1:
      yield i

def GetNSNode(obj):
  """Find the objects contain namespace declarations.

  A NS node will be an OBJson object has attribute _ns
  which stores all namespaces declaration. No other OBJson objects
  should have this attribute.
  
  Args:
    obj: an OBJson object.
  """
  SanityCheck(obj)
  alist = []
  for i in _GetNSInfo(obj):
    if i[0] not in alist:
      alist.append(i[0])
  if alist:
    return alist
  else:
    return None

def NameSpace(obj):
  """Function to return NameSpaces list used in the JSON feed.

  Args:
    obj: the OBJson object. MUST be the top node or feed node
         cause that's where NameSpaces gets defined. any sub nodes
         will not get any value.

  Returns:
    a dictionary with all NameSpaces declarations.
  """
  SanityCheck(obj)
  return dict([(i[1], i[2]) for i in _GetNSInfo(obj)])

def CleanNameSpace(obj, ns_name):
  """The clean way to remove all given namespace related element along
  with the declaration out of the given object.

  Args:
    obj: the OBJson object to operate on.
    ns_name: The namespace name string.
             Must provide full alias name such as
             'xmlns$something'. If only 'xmlns', it treates as of
             the whole namespace will be removed cause anything has
             that as the base namespace.

  Returns:
    boolean: Successfully remove returns True, otherwise, False
  """
  SanityCheck(obj)
  # Generator object. Don't put print list before using it or it
  # will become an empty list.
  ns_list = _GetNSInfo(obj)
  if not ns_list:
    return False
  else:
    for i in ns_list:
      node = i[0]
      if ns_name == i[1]:
        node.DelNameSpace(ns_name)
        if ns_name.endswith('xmlns'):
          # no application specific namespace found
          # this means to remove the whole node.
          p_node = node.ParentObj
          if not p_node:
            for i in node.GetAttrs:
              del node.__dict__[i]
            return True
          else:
            p_node.DeleteChild(node)
            return True
        else:
          alist = []
          for t in traverse(node):
            if t[0].GetName.split('$')[0] == ns_name.split('$')[1] \
                or t[0].GetName.split('$')[0] == ns_name:
              if not t[0] in alist:
                alist.append(t[0])
          for o in xrange(len(alist)):
            target = alist.pop()
            p_target = target.ParentObj
            if p_target:
                p_target.DeleteChild(target)
          if not alist:
            return True
          else:
            # Not all found elements are removed
            return False

# convert python dict to JSON string.
def ToJson(obj):
  """Wrapper function of the internal JSON feed converter.

  After convert the OBJson object back to dict, it calls
  cjson encode method to further convert it back to JSON
  raw data feed.

  Args:
    obj: OBJson object.

  Returns:
    JSON raw data string.
  """
  if isinstance(obj, objson.OBJson):
    return cjson.encode(_ToJson(obj))
  elif type(obj) is types.DictType:
    return cjson.encode(obj)
  else:
    raise objson.OBJsonTypeError, "Pass-in object %s type unknown" % obj

def ToDict(obj):
  """Wrapper function of the internal JSON feed converter.

  Converts OBJson object back to a cjson recognizable dict object.

  Args:
    obj: OBJson object.

  Returns:
    a cjson recognizable dict object.
  """
  SanityCheck(obj)
  return _ToJson(obj)

def _DoHandleURLQuote(url_string):
  """Internal function to quote url type of strings in JSON elements.

  Args:
    url_string: a string needs to be verified.

  Returns:
    A url safe string. If the string has already been quoted, it simply
    returns it.
  """
  q = urlparse.urlparse(url_string)
  if q[0] and q[1]:
    url = q[0]+"://"+q[1]
    quoted = False
    if q[2]:
      for i in _QUOTED_CHAR:
        if q[2].find(i) != -1:
          # url_string is already quoted
          quoted = True
          url += q[2]
          break
      if not quoted:
        url += urllib.quote(q[2])

    quoted = False
    if q[3]:
      for i in _QUOTED_CHAR:
        if q[3].find(i) != -1:
          quoted = True
          url += ';'+q[3]
          break
      if not quoted:
        url += ';'+urllib.quote(q[3])

    quoted = False
    if q[4]:
      for i in _QUOTED_CHAR:
        if q[4].find(i) != -1:
          quoted = True
          url += '?'+q[4]
          break
      if not quoted:
        url += '?'+urllib.quote(q[4])

    quoted = False
    if q[5]:
      for i in _QUOTED_CHAR:
        if q[5].find(i) != -1:
          quoted = True
          url += '#'+q[5]
          break
      if not quoted:
        url += '#'+urllib.quote(q[5])
    return url
  else:
    return url_string

# function to convert objson object structure to a cjson
# recognizable python dict structure
def _DoToJson(obj):
  """Function to convert an OBJson object structure back to
     JSON feed.

  Args:
    obj: The OBJson object structure

  Returns:
    JSON dict object
  """
  adict = {}
  for k in obj.GetAttrs:
    if k == '_OBJson__parent':
      continue
    v = getattr(obj, k)
    if isinstance(v, (int, long, float)):
      adict[k] = v
    elif type(v) is types.ListType:
      adict[k] = []
      for j in v:
        if isinstance(j, (int, long, float, basestring)):
          adict[k].append(j)
        else:
          adict[k].append(_ToJson(j))
    elif k == '___t':
      adict['$t'] = _HandleURLQuote(v)
    elif IsOBJson(v):
      adict[k] = _ToJson(v)
    else:
      adict[k] = _HandleURLQuote(v)
  return adict

_ToJson = psyco.proxy(_DoToJson)
_HandleURLQuote = psyco.proxy(_DoHandleURLQuote)

def display(obj):
  """Function to display the JSON object text content

  Args:
    obj: an OBJson object.
  """
  if IsOBJson(obj):
    adict = _ToJson(obj)
    pprint.pprint(adict)
  elif type(obj) is types.ListType:
    for i in obj:
      display(i)
  else:
    print str(obj)

# vim :set ts=2 sw=2 expandtab
# The End
